﻿using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Linq;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;

using System.Web;
using System.Collections.Generic;
using System.Net;
using System.IO;
using System.Configuration;
using System.Threading;
using LocalEvents.Business.Objects;
using System.Xml.Linq;
using System.Web.Caching;

namespace LocalEvents.Business
{
	public sealed partial class SearchWorkflow: SequentialWorkflowActivity
	{
        // Constants
        private const int MAX_SYNC_CALLS = 3;
        private const int UPCOMING_SEQUENCE = 0;
        private const int EVENTFUL_SEQUENCE = 1;
        private const int ZVENTS_SEQUENCE = 2;
        private const string YAHOO_API_URL = "http://api.local.yahoo.com/MapsService/V1/geocode?appid={0}&location={1}";
        private const string EVENTFUL_API_URL = "http://api.evdb.com/rest/events/search?app_key={0}&user={1}&password={2}&date=Future&location={3}";
        private const string ZVENTS_API_URL = "http://www.zvents.com/rest/search?key={0}&limit=25&where={1}";
        private const string UPCOMING_API_URL = "http://www.upcoming.org/services/rest/?api_key={0}&sort=start-date-asc&method=event.search&location={1}";

        // Workflow properties
        public string Location { get; set; }
        public HttpContext TheContext { get; set; }
        public LocalEvent[] LocalEventsData { get; set; }
        public string ErrorMessage { get; set; }
        public string YahooKey { get; set; }
        public string ZventsKey { get; set; }
        public string UpcomingKey { get; set; }
        public string EventfulAppKey { get; set; }
        public string EventfulUser { get; set; }
        public string EventfulPassword { get; set; }

        // Workflow variables
        private List<LocalEvent> _LocalEvents = new List<LocalEvent>();
        private string _Longitude = string.Empty;
        private string _Latitude = string.Empty;
        List<ManualResetEvent> locks = new List<ManualResetEvent>(MAX_SYNC_CALLS);
        ManualResetEvent[] threadEvents = new ManualResetEvent[MAX_SYNC_CALLS];

		public SearchWorkflow()
		{
			InitializeComponent();

            // Initialize the thread array
            for(int i=0; i<threadEvents.Length; ++i)
                threadEvents[i] = new ManualResetEvent(false);
        }

        #region Workflow Activity methods

        private void IsNotInCache(object sender, ConditionalEventArgs e)
        {
            // Determine whether it's available in Cache
            e.Result = TheContext.Cache[Location.ToUpper()] == null;

            // Yes available, return the result from Cache
            // And do not proceed any further of the Workflow
            if (e.Result == false)
                LocalEventsData = (LocalEvent[])TheContext.Cache[Location.ToUpper()];
        }

        private void IsInvalidLocation(object sender, ConditionalEventArgs e)
        {
            // A try catch is required.
            // Yahoo throws Bad Request (400) 
            // while it fails to recognize a location

            try
            {
                string response = GetResponse(string.Format(YAHOO_API_URL, YahooKey, Location));

                XNamespace yahooNamespace = "urn:yahoo:maps";
                var xml = XElement.Parse(response);
                var resultTag = xml.Element(yahooNamespace + "Result");
                _Latitude = resultTag.Element(yahooNamespace + "Latitude").Value;
                _Longitude = resultTag.Element(yahooNamespace + "Longitude").Value;

                e.Result = false;
            }
            catch (WebException wex)
            {
                ErrorMessage = "The location entered can not be resolve. Please make sure you typed correctly.";
                e.Result = true;
            }
        }

        private void InvokeSearchEventful(object sender, EventArgs e)
        {
            // Queue to the ThreadPool
            locks.Add(threadEvents[EVENTFUL_SEQUENCE]);
            ThreadPool.QueueUserWorkItem(new WaitCallback(SearchEventful), (object)(new object[] { threadEvents[EVENTFUL_SEQUENCE] }));
        }

        private void InvokeSearchUpcoming(object sender, EventArgs e)
        {
            locks.Add(threadEvents[UPCOMING_SEQUENCE]);
            ThreadPool.QueueUserWorkItem(new WaitCallback(SearchUpcoming), (object)(new object[] { threadEvents[UPCOMING_SEQUENCE] }));
        }

        private void InvokeSearchZvents(object sender, EventArgs e)
        {
            locks.Add(threadEvents[ZVENTS_SEQUENCE]);
            ThreadPool.QueueUserWorkItem(new WaitCallback(SearchZvents), (object)(new object[] { threadEvents[ZVENTS_SEQUENCE] }));
        }

        private void ConsolidateResult(object sender, EventArgs e)
        {
            // Wait till all the spawned threads are done
            WaitHandle.WaitAll(locks.ToArray());

            // Sort the events by EventDateTime
            _LocalEvents.Sort(new LocalEventComparer());

            _LocalEvents.ForEach(delegate(LocalEvent localEvent)
            {
                DateTime dateTime = Convert.ToDateTime(localEvent.EventDateTime);
                localEvent.EventDateTime = dateTime.Day + "/" + dateTime.Month + "/" + dateTime.Year;

                // Strip off the HTML tags and keep the Summary in 15 words
                localEvent.Summary = (localEvent.Summary != string.Empty) ? HtmlHelper.FilterWords(HtmlHelper.FilterHtml(localEvent.Summary), 15) : "No summary available.";
            });

            LocalEventsData = new LocalEvent[_LocalEvents.Count];
            _LocalEvents.CopyTo(LocalEventsData);

            // Cache the resultant array
            TheContext.Cache.Insert(Location.ToUpper(), LocalEventsData, null, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration);
        }

        #endregion 

        #region Search Methods

        private void SearchEventful(object states)
        {
            var evt = (ManualResetEvent)((object[])states)[0];

            // Retreive result from API call
            string response = GetResponse(string.Format(EVENTFUL_API_URL, EventfulAppKey, EventfulUser, EventfulUser, Location));

            var events = XElement.Parse(response).Element("events").Elements("event");
            foreach (XElement anEvent in events)
            {
                LocalEvent localEvent = new LocalEvent();

                // These APIs often return data outdated date, so filter
                localEvent.EventDateTime = DateTime.Parse(anEvent.Element("start_time").Value).ToString();
                if (!IsAcceptableDate(Convert.ToDateTime(localEvent.EventDateTime)))
                    continue;

                localEvent.Title = anEvent.Element("title").Value;
                localEvent.Summary = anEvent.Element("description").Value;
                localEvent.URL = "http://eventful.com/events/" + anEvent.Attribute("id").Value;

                // If no longitude, latitude is found, set it to what 
                // was returned by Yahoo Geocode API
                localEvent.Longitude = anEvent.Element("longitude").Value == string.Empty ? _Longitude : anEvent.Element("longitude").Value;
                localEvent.Latitude = anEvent.Element("latitude").Value == string.Empty ? _Longitude : anEvent.Element("latitude").Value;

                // If it is not already in our list, add it.
                // It is often observed that users 
                // often post the same entries in multiple sites.
                if (!_LocalEvents.Contains(localEvent))
                    _LocalEvents.Add(localEvent);
            }

            evt.Set();
        }

        private void SearchZvents(object state)
        {
            object[] pair = (object[])state;
            ManualResetEvent evt = (ManualResetEvent)pair[0];

            string response = GetResponse(string.Format(ZVENTS_API_URL, ZventsKey, Location));

            var events = XElement.Parse(response).Elements("event");
            var venues = XElement.Parse(response).Elements("venue");
            foreach (XElement anEvent in events)
            {
                LocalEvent localEvent = new LocalEvent();

                string startTime = anEvent.Element("starttime").Value;
                localEvent.EventDateTime = new DateTime(int.Parse(startTime.Substring(0, 4)), int.Parse(startTime.Substring(4, 2)), int.Parse(startTime.Substring(6, 2))).ToString();
                if (!IsAcceptableDate(Convert.ToDateTime(localEvent.EventDateTime)))
                    continue;

                localEvent.Title = anEvent.Element("name").Value;
                localEvent.Summary = anEvent.Element("description").Value;
                localEvent.URL = anEvent.Element("link").Value;

                var venueId = anEvent.Element("venue_id").Value;
                var venue = (from aVenue in venues where aVenue.Attribute("id").Value == venueId select aVenue).Single<XElement>();

                if (venue == null || venue.Element("latitude").Value == null || venue.Element("longitude").Value == null)
                {
                    localEvent.Longitude = _Longitude;
                    localEvent.Latitude = _Latitude;
                }
                else
                {
                    localEvent.Longitude = venue.Element("longitude").Value;
                    localEvent.Latitude = venue.Element("latitude").Value;
                }

                if (!_LocalEvents.Contains(localEvent))
                    _LocalEvents.Add(localEvent);
            }

            evt.Set();
        }

        private void SearchUpcoming(object state)
        {
            object[] pair = (object[])state;
            ManualResetEvent evt = (ManualResetEvent)pair[0];

            string response = GetResponse(string.Format(UPCOMING_API_URL, UpcomingKey, Location));

            var events = XElement.Parse(response).Elements("event");
            foreach (XElement anEvent in events)
            {
                LocalEvent localEvent = new LocalEvent();

                localEvent.EventDateTime = DateTime.Parse(anEvent.Attribute("start_date").Value).ToString();
                if (!IsAcceptableDate(Convert.ToDateTime(localEvent.EventDateTime)))
                    continue;

                localEvent.Title = anEvent.Attribute("name").Value;
                localEvent.Summary = anEvent.Attribute("description").Value;
                localEvent.URL = "http://upcoming.org/event/" + anEvent.Attribute("id").Value;
                localEvent.Longitude = anEvent.Attribute("longitude").Value == string.Empty ? _Longitude : anEvent.Attribute("longitude").Value;
                localEvent.Latitude = anEvent.Attribute("latitude").Value == string.Empty ? _Longitude : anEvent.Attribute("latitude").Value;

                if (!_LocalEvents.Contains(localEvent))
                    _LocalEvents.Add(localEvent);
            }

            evt.Set();
        }

        #endregion 

        #region Utilities
        
        private bool IsAcceptableDate(DateTime dateTime)
        {
            if (DateTime.Now.Date <= dateTime.Date)
                return true;
            else
                return false;
        }

        private string GetResponse(string url)
        {
            string content = string.Empty;

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();

            using (Stream reader = response.GetResponseStream())
            {
                using (StreamReader responseReader = new StreamReader(reader))
                {
                    content = responseReader.ReadToEnd();
                    responseReader.Close();
                }

                reader.Close();
            }

            return content;
        }

        private class LocalEventComparer : IComparer<LocalEvent>
        {
            public int Compare(LocalEvent x, LocalEvent y)
            {
                return Convert.ToDateTime(x.EventDateTime).CompareTo(Convert.ToDateTime(y.EventDateTime));
            }
        }


        #endregion
    }

}
